package org.sith.gwt;


import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ProxyGenerator {


    private DocumentBuilder documentBuilder;
    private static final String PROXY_EXT = "Proxy";

    private final List<Class> enumsClazzes = new ArrayList<Class>();
    private final List<Class> clazzList;
    private File parentDir;
    private final String targetPackage;
    private Transformer classProxyTransformer;
    private File distFolder;
    private TransformerFactory transformerFactory;
    private Transformer markerProxyInterfaceTransformer;
    private Transformer enumProxyTransformer;


    public ProxyGenerator(List<Class> clazzList, File parentDir, String targetPackage) {
        this.clazzList = clazzList;
        this.parentDir = parentDir;
        this.targetPackage = targetPackage;


        try {
            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
            documentBuilder = builderFactory.newDocumentBuilder();

            transformerFactory = TransformerFactory.newInstance();
            InputStream proxyXsl = getClass().getClassLoader().getResourceAsStream("proxy.xsl");
            classProxyTransformer = transformerFactory.newTransformer(new StreamSource(proxyXsl));

            InputStream enumProxyXsl = getClass().getClassLoader().getResourceAsStream("enum.xsl");
            enumProxyTransformer = transformerFactory.newTransformer(new StreamSource(enumProxyXsl));


            InputStream proxyMarkerXsl = getClass().getClassLoader().getResourceAsStream("proxyMarker.xsl");
            markerProxyInterfaceTransformer = transformerFactory.newTransformer(new StreamSource(proxyMarkerXsl));


            distFolder = new File(parentDir, targetPackage.replace(".", "/"));
            distFolder.mkdirs();
        } catch (ParserConfigurationException e) {
            throw new IllegalStateException(e);
        } catch (TransformerConfigurationException e) {
            throw new IllegalStateException(e);
        }
    }

    public void generateProxy() {


        createProxyMarkerInterface();

        for (Class clazz : clazzList) {
            if (clazz.isInterface()) {
                continue;
            }


            Method[] methods = clazz.getDeclaredMethods();
            Map<String, Method> methodsMap = new HashMap<String, Method>();
            for (Method method : methods) {
                methodsMap.put(method.getName().toLowerCase(), method);
            }

            Document document = documentBuilder.newDocument();
            Element objectElement = document.createElement("object");

            document.appendChild(objectElement);

            String proxyClazzName = getClassName(clazz) + PROXY_EXT;
            objectElement.setAttribute("clazzName", proxyClazzName);
            objectElement.setAttribute("package", targetPackage);

            Field[] fields = clazz.getDeclaredFields();
            for (Field field : fields) {
                Element fieldElement = document.createElement("field");
                objectElement.appendChild(fieldElement);
                fieldElement.setAttribute("name", field.getName());


                fieldElement.setAttribute("type", getTypeName(field));

                addAccesMethodAttribute(methodsMap, field, fieldElement, AccessType.GET);
                addAccesMethodAttribute(methodsMap, field, fieldElement, AccessType.SET);
                addAccesMethodAttribute(methodsMap, field, fieldElement, AccessType.IS);

            }


            writeClass(proxyClazzName, document, classProxyTransformer);


        }

        for (Class enumsClazz : enumsClazzes) {
            Document document = documentBuilder.newDocument();
            Element anEnum = document.createElement("enum");
            String clazzName = getClassName(enumsClazz) + PROXY_EXT;
            anEnum.setAttribute("name", clazzName);
            anEnum.setAttribute("package", targetPackage);
            document.appendChild(anEnum);

            Field[] declaredFields = enumsClazz.getFields();
            for (Field declaredField : declaredFields) {
                Element field = document.createElement("field");
                anEnum.appendChild(field);
                field.setTextContent(declaredField.getName());
            }

            writeClass(clazzName, document, enumProxyTransformer);
        }


    }

    private void createProxyMarkerInterface() {
        Document proxyMarkerDocument = documentBuilder.newDocument();
        Element proxyMarker = proxyMarkerDocument.createElement("proxyMarker");
        proxyMarkerDocument.appendChild(proxyMarker);
        proxyMarker.setAttribute("package", targetPackage);

        writeClass("ProxyMarker", proxyMarkerDocument, markerProxyInterfaceTransformer);
    }

    private void writeClass(String proxyClazzName, Document document, Transformer proxyTransformer) {
        try {
            DOMSource source = new DOMSource(document);
            File destFile = new File(distFolder, proxyClazzName + ".java");
            FileOutputStream os = new FileOutputStream(destFile);
            StreamResult result = new StreamResult(os);
            proxyTransformer.transform(source, result);
            os.close();
        } catch (TransformerException e) {
            throw new IllegalStateException(e);
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    private String getClassName(Class clazz) {
        String name = clazz.getName();
        return name.substring(name.lastIndexOf(".") + 1);
    }

    private String getTypeName(Field field) {
        Class<?> type = field.getType();


        if (type.isEnum()) {
            enumsClazzes.add(type);
            return getClassName(type) + PROXY_EXT;
        }

        if (type.isPrimitive()) {
            return getWrapperClassForPrimitives(type);
        }

        String genericPart = getGenericPart(field);


        StringBuilder builder = new StringBuilder();

        if (clazzList.contains(type)) {
            builder.append(getClassName(type)).append(PROXY_EXT).toString();
        } else {
            builder.append(type.getCanonicalName());
        }

        builder.append(genericPart);


        return builder.toString();
    }

    private String getWrapperClassForPrimitives(Class<?> type) {
        String canonicalName = type.getCanonicalName();
        if ("boolean".equals(canonicalName)) {
            return Boolean.class.getCanonicalName();
        } else if ("int".equals(canonicalName)) {
            return Integer.class.getCanonicalName();
        } else if ("long".equals(canonicalName)) {
            return Long.class.getCanonicalName();
        } else {
            throw new IllegalStateException("Unknown primitive type:" + canonicalName);
        }


    }

    private String getGenericPart(Field field) {
        Type genericType = field.getGenericType();
        if (genericType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericType;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            StringBuilder builder = new StringBuilder();
            builder.append("<");
            for (Type generic : actualTypeArguments) {

                Class genericClazz = (Class) generic;
                if (clazzList.contains(genericClazz)) {
                    builder.append(getClassName(genericClazz) + PROXY_EXT);

                } else {
                    builder.append(genericClazz.getCanonicalName());
                }
                builder.append(",");
            }
            builder.setCharAt(builder.length() - 1, '>');
            return builder.toString();
        }
        return "";
    }

    private void addAccesMethodAttribute(Map<String, Method> methodsMap, Field field, Element fieldElement, AccessType accessType) {
        Method method = methodsMap.get((accessType.name() + field.getName()).toLowerCase());
        if (method != null) {
            fieldElement.setAttribute(accessType.getAttributeName(), method.getName());
        }
    }
}
